"""Revision management for Reversion."""


try:
    set
except NameError:
    from sets import Set as set  # Python 2.3 fallback.

try:
    from threading import local
except ImportError:
    from django.utils._threading_local import local  # Python 2.3 fallback.

try:
    from functools import wraps
except ImportError:
    from django.utils.functional import wraps  # Python 2.3, 2.4 fallback.

from django.contrib.contenttypes.models import ContentType
from django.core import serializers
from django.db.models.signals import post_save

from reversion.registration import registry
from reversion.models import Revision, Version


class RevisionManagementError(Exception):
    
    """
    Exception that is thrown when something goes wrong with revision management.
    """
          
          
class RevisionManager(local):
    
    """Manages the creation of revisions."""
    
    def __init__(self):
        """Initializes the revision manager."""
        self._depth = 0
        self._clear()
        post_save.connect(self.post_save_receiver)

    def _clear(self):
        """Clears the state of the revision manager."""
        self._objects = set()
        self._user = None
        self._comment = None
        self._dirty = False
        self._meta = []

    # Low-level revision management methods.
    
    def enter_revision_management(self):
        """
        Begins a block of revision management for the current thread.
        
        This MUST be balanced by a call to `leave_revision_management`.  It is
        recommended that you leave these methods alone and instead use the
        revision context manager or the `create_on_success` decorator.
        """
        self._depth += 1
        
    def is_managed(self):
        """Returns whether the current thread is under revision managment."""
        return self._depth > 0
    
    def assert_managed(self):
        """
        Throws an exception if the current thread is not under revision
        management.
        """
        if not self.is_managed():
            raise RevisionManagementError, "There is no active revision for this thread."

    def is_dirty(self):
        """Checks whether the current revision has any version data."""
        self.assert_managed()
        return self._dirty

    def add_object(self, obj):
        """
        Adds an object to the current revision.
        
        There is normally no need to call this method, as it is called
        automatically when a registered model is saved.
        """
        registry.assert_registered(obj.__class__)
        self.assert_managed()
        self._objects.add(obj)
        self._dirty = True
        
    def remove_object(self, obj):
        """Removes a model from the current revision."""
        self.assert_managed()
        try:
            self._objects.remove(obj)
        except KeyError:
            raise RevisionManagementError, "%r is not in the current revision." % obj
        
    def set_user(self, user):
        """Sets the user for the current revision"""
        self.assert_managed()
        self._user = user
        
    def get_user(self):
        """Gets the user for the current revision."""
        self.assert_managed()
        return self._user
    
    user = property(get_user,
                    set_user,
                    doc="The user for the current revision.")
        
    def set_comment(self, comment):
        """Sets the comment for the current revision"""
        self.assert_managed()
        self._comment = comment
        
    def get_comment(self):
        """Gets the comment for the current revision."""
        self.assert_managed()
        return self._comment
    
    comment = property(get_comment,
                       set_comment,
                       doc="The comment for the current revision.")
        
    def add_meta(self, cls, **kwargs):
        """Adds a class of meta information to the current revision."""
        self.assert_managed()
        self._meta.append((cls, kwargs))
        
    def rollback(self):
        """Clears the current version data."""
        self.assert_managed()
        self._clear()
        
    def commit(self):
        """Writes the current version data to a new revision."""
        self.assert_managed()
        revision_set = registry.get_revision_set(self._objects)
        try:
            if revision_set:
                # Save a new revision.
                revision = Revision.objects.create(user=self._user,
                                                   comment=self._comment)
                # Save version models.
                for obj in revision_set:
                    registration_info = registry.get_registration_info(obj.__class__)
                    object_id = unicode(obj.pk)
                    content_type = ContentType.objects.get_for_model(obj)
                    serialized_data = serializers.serialize(registration_info.format, [obj], fields=registration_info.fields)
                    Version.objects.create(revision=revision,
                                           object_id=object_id,
                                           content_type=content_type,
                                           format=registration_info.format,
                                           serialized_data=serialized_data,
                                           object_repr=unicode(obj))
                # Save meta information.
                for cls, kwargs in self._meta:
                    cls._default_manager.create(revision=revision, **kwargs)
        finally:
            self._clear()
        
    def leave_revision_management(self):
        """Ends a revision."""
        self.assert_managed()
        if self.is_dirty():
            raise RevisionManagementError, "Revision managed block ended with an open revision."
        self._depth -= 1
        
    # Signal receivers.
        
    def post_save_receiver(self, instance, sender, **kwargs):
        """Adds registered models to the current revision, if any."""
        if registry.is_registered(sender):
            if self.is_managed():
                self.add_object(instance)
       
    # High-level revision management methods.
        
    def __enter__(self):
        """Enters a block of revision management."""
        self.enter_revision_management()
        
    # TODO: place leave_revision_management in try, finally block.
    def __exit__(self, exc_type, exc_value, traceback):
        """Leaves a block of revision management."""
        if exc_type is None:
            if self.is_dirty():
                self.commit()
        else:
            if self.is_dirty():
                self.rollback()
        self.leave_revision_management()
        return False
        
    def create_manually(self, func):
        """
        Allows revisions to be created manually within the context of this
        function.
        """
        def _create_manually(*args, **kwargs):
            self.enter_revision_management()
            try:
                result = func(*args, **kwargs)
            finally:
                self.leave_revision_management()
            return result
        return wraps(func)(_create_manually)
        
    # TODO: place leave_revision_management in try, finally block.
    def create_on_success(self, func):
        """Creates a revision when the given function exist successfully."""
        def _create_on_success(*args, **kwargs):
            self.enter_revision_management()
            try:
                result = func(*args, **kwargs)
            except:
                if self.is_dirty():
                    self.rollback()
                raise
            else:
                if self.is_dirty():
                    self.commit()
            self.leave_revision_management()
            return result
        return wraps(func)(_create_on_success)


# A thread-safe shared revision manager.
revision = RevisionManager()

